#region
/* Adoor.NET - Object/Relational mapping framework leveraging ADO.NET`s powerful disconnected mode.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *  
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
#endregion

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Xml;
using Adoor.Data;
using Adoor.Expression;
using Adoor.Expression.AST;
using Adoor.Object.Disconnected;
using Adoor.Object.Domain;
using Adoor.Object.Entity;
using Adoor.Object.Query;
using Adoor.Relational;

namespace Adoor.Object.Relational
{
    public class LookupTableMapping<TReference> : MultipleReferenceMapping<TReference>, IReferenceMappingStrategyProvider, IReferenceMappingStrategy
    {
        public LookupTableMapping(ReferenceData referenceData, string sourceForeignKey, string targetForeignKey, string tableName)
            :
            base(referenceData, sourceForeignKey, targetForeignKey)
        {
            this.tableName = tableName;

            this.ReferenceData.RegisterService(typeof(IReferenceMappingStrategyProvider), this);
            this.ReferenceData.RegisterService(typeof(IReferenceMappingStrategy), this);
        }

        public ICollection<DataColumn> DataColumns
        {
            get
            {
                if (this.dataColumns == null)
                {
                    IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
                    IEntityMappingStrategyProvider targetProvider = (IEntityMappingStrategyProvider)this.ReferenceData.TargetData.GetService(typeof(IEntityMappingStrategyProvider));
                    IEntityMappingStrategy sourceMappingStrategy = sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2); // TODO: remove MappingStrategyEnum.Remote
                    IEntityMappingStrategy targetMappingStrategy = targetProvider.GetMappingStrategy(MappingStrategyEnum.Remote2); // TODO: remove MappingStrategyEnum.Remote
                    this.dataColumns = new DataColumn[]
					{
						new DataColumn(this.SourceForeignKey, sourceMappingStrategy.KeyMappingStrategy.DataColumns[0].DataType),
						new DataColumn(this.TargetForeignKey, targetMappingStrategy.KeyMappingStrategy.DataColumns[0].DataType)
					};
                }
                return this.dataColumns;
            }
        }

        public string TableName
        {
            get { return this.tableName; }
        }

        public RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string correlation, string alias)
        {
            string temp_alias = correlation + "_" + alias;

            IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
            IEntityMappingStrategy sourceMappingStrategy = sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2); // TODO: remove MappingStrategyEnum.Remote
            RelationalOperator join1 = new JoinOperator(isOuterJoin, source,
                sourceMappingStrategy.KeyMappingStrategy.PrimaryKey,
                new ScanQuery(this.TableName, this.DataColumns), this.SourceForeignKey,
                correlation, temp_alias);

            RelationalOperator result = new JoinOperator(isOuterJoin,
                join1, this.TargetForeignKey,
                targetQuery, targetKey.PrimaryKey,
                temp_alias, alias);

            return result;
        }

        public RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string correlation, string alias, bool partial, EntityData sourceEntity)
        {
            if (partial)
            {
                string temp_alias = correlation + "_" + alias;

                IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
                IEntityMappingStrategy sourceMappingStrategy = sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2); // TODO: remove MappingStrategyEnum.Remote
                RelationalOperator join1;
                if (this.ReferenceData.SourceData == sourceEntity)
                {
                    join1 = new JoinOperator(isOuterJoin, source,
                        sourceMappingStrategy.KeyMappingStrategy.PrimaryKey,
                        new ScanQuery(this.TableName, this.DataColumns), this.SourceForeignKey,
                        correlation, temp_alias);
                }
                else
                    join1 = new JoinOperator(isOuterJoin, source,
                        sourceMappingStrategy.KeyMappingStrategy.PrimaryKey,
                        new ScanQuery(this.TableName, this.DataColumns), this.TargetForeignKey,
                        correlation, temp_alias);

                return join1;
            }
            return Join(isOuterJoin, source, targetQuery, targetKey, correlation, alias);
        }

        public RelationalOperator Cross(RelationalOperator source, RelationalQuery targetQuery, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin)
        {
            string temp_alias = correlation + "_" + alias;

            IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
            IEntityMappingStrategy sourceMappingStrategy = sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2); // TODO: remove MappingStrategyEnum.Remote
            RelationalOperator join1 = new JoinOperator(isOuterJoin, source,
                sourceMappingStrategy.KeyMappingStrategy.PrimaryKey,
                new ScanQuery(this.TableName, this.DataColumns), this.SourceForeignKey,
                correlation, temp_alias);

            return new CrossOperator(join1, targetQuery, alias, joinCondition, isOuterJoin);
        }

        public RelationalOperator Cross(RelationalOperator source, RelationalOperator source2, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin)
        {
            IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
            IEntityMappingStrategy sourceMappingStrategy = sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2); // TODO: remove MappingStrategyEnum.Remote

            string temp_alias = correlation + "_" + alias;


            CrossOperator co = null;
            joinCondition = ExpressionReplace.ReplaceNameQualifer(joinCondition, this.ReferenceData.SourceData.EntityType.Name, temp_alias);
            joinCondition = new ExpressionBinary(ScannerToken.And, joinCondition,
                new ExpressionBinary(ScannerToken.Eq
                , new ExpressionName(temp_alias, this.SourceForeignKey)
                , new ExpressionName(this.ReferenceData.SourceData.EntityType.Name, sourceMappingStrategy.KeyMappingStrategy.PrimaryKey)));
            co = new CrossOperator(source, new AliasOperator(new ScanQuery(this.TableName, this.DataColumns), temp_alias)
                , temp_alias, null, isOuterJoin);
            co = new CrossOperator(co,
                source2, alias, joinCondition, isOuterJoin);
            return co;
        }

        public override void FetchReference(ObjectContext context, ObjectPath path, object args2, string root, string alias, bool isGloballyLoaded)
        {
            string correlation = root + "_" + alias;
            object args = RelationalQueryGenerator.GenerateArgs(args2);
            RelationalOperator op = RelationalQueryGenerator.GenerateOperator(MappingStrategyEnum.Remote2, path);

            // in case of outer join, this returns (null, null)
            //			RelationalQuery rq = op.Project(
            //					new SelectExpression(this.TargetForeignKey, new ExpressionName(correlation, this.TargetForeignKey)),
            //					new SelectExpression(this.SourceForeignKey, new ExpressionName(correlation, this.SourceForeignKey)));
            //			rq.Distinct = true; // Distinct=true will always work since we only have foreign key columns

            // in case of outer join, we do not want to get (null, null) rows for empty references
            // rather, we want (source, null), so that we can still mark source as globally loaded
            IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
            IEntityMappingStrategy sourceMappingStrategy = sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2);
            RelationalQuery rq = op.Project(
                // new SelectExpression(this.SourceForeignKey, new ExpressionName(correlation, this.SourceForeignKey)));
                new SelectExpression(this.SourceForeignKey, new ExpressionName(root, sourceMappingStrategy.KeyMappingStrategy.PrimaryKey)),
                new SelectExpression(this.TargetForeignKey, new ExpressionName(correlation, this.TargetForeignKey)));
            rq.Distinct = true; // Distinct=true will always work sin we only have primary and foreign key columns

            // TODO: remove cast (use generics in C# 2)
            RelationalObjectSource orRemote = (RelationalObjectSource)this.ReferenceData.SourceData.Domain.DomainGroup.RemoteSource;
            if (orRemote == null)
                throw new Exception("Remote source not found for entity " + this.ReferenceData.SourceData.EntityType.FullName);

            DataTable table = DataUtils.CreateCaseInsesitiveDataTable();  // TODO: use a DataReader
            orRemote.RelationalSource.Fill(rq, args, table);
            foreach (DataRow dr in table.Rows)
                if (dr.IsNull(1)) // possible for an empty outer join
                    dr.Delete();

            //			// particular case: when preloading an outer join reference with no result, the table will contain a unique row with DBNulls
            //			if (table.Rows.Count == 1 && table.Rows[0][0] == DBNull.Value && table.Rows[0][1] == DBNull.Value)
            //				table.Rows.RemoveAt(0);
            //
            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage storage = (DataSetObjectStorage)context.LocalStorage;

            DataView view = new DataView(table);
            storage.FillTable(this.TableName, this.DataColumns, view);
            //System.Diagnostics.Trace.WriteLine(view.Count.ToString() + " references loaded", "SQL");

            if (isGloballyLoaded)
            {
                table.RejectChanges(); // recover the empty outer joins
                view.Sort = this.SourceForeignKey;
                object prevKey = null;
                IOidData oidData = this.ReferenceData.SourceData.OidData;
                foreach (DataRowView drv in view)
                {
                    object curKey = drv.Row[this.SourceForeignKey];
                    if (!curKey.Equals(prevKey) && !DBNull.Value.Equals(curKey))
                    {
                        KeyValue key = new KeyValue(new object[] { curKey });
                        // in case of inheritance, this.TargetMapping may the ancestor's descriptor
                        // we must find the actual factory matching "key"
                        // TODO: or could we move CompleteReference up to IEntityMapping ?
                        IKeyMappingStrategy keyMappingStrategy = (IKeyMappingStrategy)oidData.GetService(typeof(IKeyMappingStrategy));
                        string oid = keyMappingStrategy.GetOid(key);

                        //						EntityData entityData = oidData.GetActualEntityData(oid);
                        //						Debug.Assert(!entityData.IsAbstract);
                        //						IEntityLoader loader = (IEntityLoader)entityData.GetService(typeof(IEntityLoader));
                        IEntityLoader loader = (IEntityLoader)this.ReferenceData.SourceData.GetService(typeof(IEntityLoader));

                        loader.CompleteReference(context.LocalStorage, oid, this.ReferenceData.TargetRole, PreloadFlags.Reference | PreloadFlags.TargetEntities);
                        //System.Diagnostics.Trace.WriteLine("Mark " + this.ReferenceData.SourceData.EntityType.Name + "(" + curKey.ToString() + ")." + this.ReferenceData.TargetRole, "SQL");
                        prevKey = curKey;
                    }
                }
            }
        }

        protected bool FetchRelation(IEntity sourceEntity, bool markRelation)
        {
            PreloadFlags flags = PreloadFlags.Reference;
            if (sourceEntity.State.IsReferenceComplete(this.ReferenceData.TargetRole, ref flags))
                return false;

            ObjectPath source = this.ReferenceData.SourceData.GetPath(
                this.ReferenceData.SourceData.EntityType.Name
                ).Where("[" + this.ReferenceData.SourceData.EntityType.Name + "] = ?");

            string temp_alias = this.ReferenceData.SourceData.EntityType.Name + "_" + this.ReferenceData.TargetRole;

            System.Collections.ICollection args = RelationalQueryGenerator.GenerateArgs(new object[] { sourceEntity });
            RelationalOperator command = RelationalQueryGenerator.GenerateOperator(MappingStrategyEnum.Remote2, source);

            IEntityMappingStrategyProvider sourceProvider = (IEntityMappingStrategyProvider)this.ReferenceData.SourceData.GetService(typeof(IEntityMappingStrategyProvider));
            IEntityMappingStrategy sourceMappingStrategy = sourceProvider.GetMappingStrategy(MappingStrategyEnum.Remote2);

            command = new JoinOperator(false,
                command, sourceMappingStrategy.KeyMappingStrategy.PrimaryKey,
                new ScanQuery(this.TableName, this.DataColumns), this.SourceForeignKey,
                this.ReferenceData.SourceData.EntityType.Name, temp_alias);
            RelationalQuery rq = command.Project(
                new SelectExpression(this.TargetForeignKey, new ExpressionName(temp_alias, this.TargetForeignKey)),
                new SelectExpression(this.SourceForeignKey, new ExpressionName(temp_alias, this.SourceForeignKey)));
            rq.Distinct = true;

            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;
            ObjectContext context = storage.Context;

            // TODO: remove cast (use generics in C# 2)			
            RelationalObjectSource orRemote = (RelationalObjectSource)this.ReferenceData.SourceData.Domain.DomainGroup.RemoteSource;
            if (orRemote == null)
                throw new Exception("Remote source not found for entity " + this.ReferenceData.SourceData.EntityType.FullName);

            DataTable table = DataUtils.CreateCaseInsesitiveDataTable();
            orRemote.RelationalSource.Fill(rq, args, table);  // Distinct() is correct since we only have foreign key columns


            storage.FillTable(this.TableName, this.DataColumns, new DataView(table));

            if (markRelation)
                sourceEntity.State.CompleteReference(this.ReferenceData.TargetRole, PreloadFlags.Reference);
            return true;
        }

        public override IList<TReference> Collect(IEntity sourceEntity)
        {
            return new ReferenceList<TReference>(this.ReferenceData, sourceEntity);
        }

        public override System.Collections.IEnumerator EnumTargetKeys(IEntity sourceEntity)
        {
            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;
            ObjectContext context = storage.Context;
            if (context != null && !context.Disconnected)
                FetchRelation(sourceEntity, true);


            DataView index = storage.GetIndex(this.TableName, this.SourceForeignKey);
            if (index == null)
                return (new object[0]).GetEnumerator();  // TODO: EmptyEnumerator.Value
            DataRowView[] rowViews = index.FindRows(((IRelationalEntityState)sourceEntity.State).Key.Values);
            List<KeyValue> result = new List<KeyValue>(rowViews.Length);
            foreach (DataRowView drv in rowViews)
            {
                if (drv.Row.RowState != DataRowState.Deleted)
                    result.Add(new KeyValue(drv[this.TargetForeignKey]));
            }
            return result.GetEnumerator();
        }

        public override void InitInverseReferenceReader<TInverse>(ReferenceData inverseReferenceData)
        {
            new LookupTableMapping<TInverse>(inverseReferenceData, this.SourceForeignKey, this.TargetForeignKey, this.TableName);
        }

        public IReferenceMappingStrategy GetMappingStrategy(MappingStrategyEnum ms)
        {
            return this;
        }

        public ExpressionNode GetRelationalExpression(string correlation, string value, string alias)
        {
            if (correlation == null)
                return new ExpressionName(TableName, value);

            return new ExpressionName(correlation + "_" + alias, value);
        }

        private string tableName;
        protected string updateCommand;
        protected string insertCommand;
        protected string deleteCommand;
        private ICollection<DataColumn> dataColumns;
    }

    public class LookupTableFactory<TReference> : LookupTableMapping<TReference>
        , IReferenceWriter<TReference>
        , ISingleReferenceWriter<TReference>
    {
        public LookupTableFactory(ReferenceData referenceData, string sourceForeignKey, string targetForeignKey, string tableName)
            :
            base(referenceData, sourceForeignKey, targetForeignKey, tableName)
        {
            this.ReferenceData.RegisterService(typeof(IReferenceWriter), this);
            this.ReferenceData.RegisterService(typeof(IReferenceWriter<TReference>), this);
            if (this.ReferenceData.IsSingleReference)
            {
                this.ReferenceData.RegisterService(typeof(ISingleReferenceWriter<TReference>), this);
            }

        }

        public LookupTableFactory(ReferenceData referenceData, XmlNode metadata)
            :
            this(referenceData, GetSourceForeignKey(metadata),
            GetTargetForeignKey(metadata), GetTableName(metadata))
        {
        }

        static string GetTableName(XmlNode metadata)
        {
            return metadata.SelectSingleNode("@tableName").Value;
        }

        static string GetSourceForeignKey(XmlNode metadata)
        {
            return metadata.SelectSingleNode("@sourceForeignKey").Value;
        }

        static string GetTargetForeignKey(XmlNode metadata)
        {
            return metadata.SelectSingleNode("@targetForeignKey").Value;
        }

        //		static string GetTargetRole(XmlNode metadata)
        //		{
        //			return metadata.ParentNode.LocalName;
        //		}

        public void InitInverseReferenceWriter<TInverse>(ReferenceData inverseReferenceData)
        {
            new LookupTableFactory<TInverse>(inverseReferenceData, this.TargetForeignKey, this.SourceForeignKey, this.TableName);
        }

        private bool aggregate = false;  // TODO: not used
        public bool Aggregate
        {
            get { return this.aggregate; }
        }

        public void Add(IEntity sourceEntity, IEntity targetEntity,
            bool checkForIntegrity)
        {
            IRelationalEntityState sourceState = (IRelationalEntityState)sourceEntity.State;
            IRelationalEntityState targetState = (IRelationalEntityState)targetEntity.State;
            if (targetEntity.State.Storage != sourceEntity.State.Storage)
                throw new Exception("storage mismatch source '" + sourceEntity.State.Storage.Name + "' target '" + targetEntity.State.Storage.Name + "'");

            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;

            DataRow row = storage.CreateRow(TableName, this.DataColumns);
            row.BeginEdit(); // begin update composite key
            row[this.SourceForeignKey] = sourceState.Key.Values[0];
            row[this.TargetForeignKey] = targetState.Key.Values[0];
            row.EndEdit();   // end update composite key
            // to protect the list from concurrent updates, we mark sourceEntity as modified
            sourceEntity.State.Touch();
        }

        public void Remove(IEntity sourceEntity,
            IEntity targetEntity)
        {
            IRelationalEntityState sourceState = (IRelationalEntityState)sourceEntity.State;
            IRelationalEntityState targetState = (IRelationalEntityState)targetEntity.State;
            
            if (targetEntity.State.Storage != sourceEntity.State.Storage)
                throw new Exception("storage mismatch source '" + sourceEntity.State.Storage.Name + "' target '" + targetEntity.State.Storage.Name + "'");


            DataRowView[] drvs = new DataRowView[0];
            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;

            DataView index = storage.GetIndex(this.TableName, this.SourceForeignKey + "," + this.TargetForeignKey);
            object sourceKey = sourceState.Key.Values[0];
            object targetKey = targetState.Key.Values[0];
            if (index != null)
                drvs = index.FindRows(new object[] { sourceKey, targetKey });
            if (drvs.Length == 0 && storage.Context != null && !storage.Context.Disconnected && FetchRelation(sourceEntity, false))
            {
                if (index == null)
                    index = storage.GetIndex(this.TableName, this.SourceForeignKey + "," + this.TargetForeignKey);
                if (index != null)
                    drvs = index.FindRows(new object[] { sourceKey, targetKey });
            }
            //			if (rowIndex == -1)
            //				throw new Exception(GetType().Name + ".Remove: entity not found");
            if (drvs.Length == 0)
                return;
            foreach (DataRowView drv in drvs)
            {
                DataRow row = drv.Row;
                if (row.RowState != DataRowState.Deleted) // TODO: test needed ?
                    row.Delete();
            }
        }

        public new virtual IList<TReference> Collect(IEntity sourceEntity)
        {
            return new ReferenceList<TReference>(this.ReferenceData, sourceEntity);
        }

        public void Clear(IObjectStorage localStore)
        {
            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage storage = (DataSetObjectStorage)localStore;
            DataTable table = storage.DataSet.Tables[this.TableName];
            if (table != null)
                table.Clear();
        }

        public void Update(ObjectContext context)
        {
            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage storage = (DataSetObjectStorage)context.LocalStorage;

            DataTable table = storage.GetTable(this.TableName, null);
            if (table != null)
            {
                System.Threading.Monitor.Enter(table);
                try
                {
                    RelationalObjectSource orRemote = (RelationalObjectSource)this.ReferenceData.SourceData.Domain.DomainGroup.RemoteSource;
                    if (orRemote == null)
                        throw new Exception("Remote source not found for entity " + this.ReferenceData.SourceData.EntityType.FullName);


                    //this.ReferenceData.RaiseUpdateEvent(transaction);

                    List<DataColumn> cols = new List<DataColumn>();
                    foreach (DataColumn dc in table.Columns)
                    {
                        cols.Add(dc);
                    }
                    orRemote.UpdateTable(table, false,
                        this.updateCommand, this.insertCommand, this.deleteCommand, cols);
                    //				}
                    //				else
                    //					Debug.Assert(false);
                }
                finally
                {
                    System.Diagnostics.Trace.WriteLine("Thread leave " + table.TableName + " " + System.Threading.Thread.CurrentThread.GetHashCode());
                    System.Threading.Monitor.Exit(table);
                }
            }
        }

        public void Commit(ObjectContext context)
        {
            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage storage = (DataSetObjectStorage)context.LocalStorage;

            DataTable table = storage.GetTable(this.TableName, null);
            if (table != null)
            {
                table.AcceptChanges();
            }
        }

        public void Cancel(IObjectStorage storage)
        {
            // TODO: remove cast (use generics in C# 2)
            DataSetObjectStorage rstorage = (DataSetObjectStorage)storage;

            DataTable table = rstorage.GetTable(this.TableName, null);
            if (table != null)
                table.RejectChanges();
        }

        public void SetValue(IEntity sourceEntity, IEntity targetEntity)
        {
            object current = this.GetValue(sourceEntity);
            if (current != null)
                this.Remove(sourceEntity, (IEntity)current);
            if (targetEntity != null)
                this.Add(sourceEntity, targetEntity, false);
        }
    }
}
